/*
 * TMap Designer - A Thematic Map design software
 * Copyright (C) 2006 World Health Organization
 * http://www.who.int
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package who.tmap.toolkit.impl;

import java.awt.BasicStroke;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JViewport;

import org.apache.log4j.Logger;

import who.tmap.toolkit.MappingToolKit;
import who.tmap.toolkit.shape.annotation.BackgroundImageShape;

public class MappingToolKitScalingHelper {

    public static final int DEFAULT_ZOOM_FACTOR =100;	
    private static final double DEFAULT_MAP_FACTOR =1;    
    private static final int ZOOM_FACTOR_STEP =2;
    private static final int MINIMUM_ZOOM_FACTOR =1;
    
    public static final int FACTOR_X =0;
    public static final int FACTOR_Y =1;
	
    private static Logger logger = Logger.getLogger(MappingToolKitScalingHelper.class);
    
    private MappingToolKit mtk;
    private double mapFactor, mapFactorX, mapFactorY;
    private double minX, maxX, minY, maxY;
    private double digitFactorX, digitFactorY;
    private int zoomFactor;
    private Rectangle2D bounds;
    private boolean boundsHaveChanged =false;
	
	public MappingToolKitScalingHelper( MappingToolKit mtk ) {
		this.mtk =mtk;
	}
	
	public void init() {
		zoomFactor =DEFAULT_ZOOM_FACTOR;
		mapFactor =DEFAULT_MAP_FACTOR;
		mapFactorX =DEFAULT_MAP_FACTOR;
		mapFactorY =DEFAULT_MAP_FACTOR;
		
		minX =0;
		maxX =0;
		minY =0;
		maxY =0;	
		
		bounds =new Rectangle2D.Double( 0, 0, 0, 0 );
		boundsHaveChanged =false;
	}
	
	/**
	 * Apply zoom factor
	 * @param g
	 */
	public void scale( Graphics2D g ) {
        g.scale( getMapToScreenFactor(), getMapToScreenFactor() );
	}	
	
	/**
	 * Calculates a stroke width based on the zoom & display factor
	 * @param g
	 */
	public void setStrokeWidth( Graphics2D g ) {
        float strokeWidth = (float)Math.min( 1.d,
        		                             1.d / ( mapFactor * (double)zoomFactor / 100.d ) );
        
        if ( mtk.getThematicMap() != null && mtk.getThematicMap().getAnnotations() != null ) {
        	strokeWidth*=mtk.getThematicMap().getAnnotations().getBaseMapLineWidth();
        }
        
        g.setStroke(new BasicStroke(Math.abs(strokeWidth)));	
        
        //logger.debug( "stroke width=" + strokeWidth );
	}	
	
	public double processMapFactor( double width, double height ) {
	    mapFactorX =1.d;
	    mapFactorY =1.d; 
	    
        // Calculate the X factor
	    double mapWidth =Math.abs( getMaxX() - getMinX() );
        
        if ( mapWidth != 0 ) {
            mapFactorX =width / mapWidth;
        } else {
        	mapFactorX =width;
        }
        
        // Calculate the Y factor
	    double mapHeight =Math.abs( getMaxY() - getMinY() );
        
        if ( mapHeight != 0 ) {
            mapFactorY =height / mapHeight;
        } else {
        	mapFactorY =height;
        }
        
        // Final factor is the smallest factor
        mapFactor =Math.min( mapFactorX, mapFactorY );
        
        if ( mapFactor == 0d ) {
            mapFactor =1d;
        } 
         
        logger.debug( "\nbounds=" + bounds 
        			+ "\nFactor=" + mapFactor + "\nmapWidth=" + mapWidth + ", w=" + width + ", factorX=" + mapFactorX
        			+ "\nmapHeight=" + mapHeight + ", h=" + height + ", factorY=" + mapFactorY );
        
        return mapFactor;		
	}
	
	/**
	 * Compute dimensions excluding unnecessary edges
	 * @param width
	 * @param height
	 * @return
	 */
	public Dimension getDimForEdgeRemoval( int width, int height ) {
		Dimension dim =new Dimension();
		
        if ( mapFactor == mapFactorX ) {
    	    double mapHeight =Math.abs( getMaxY() - getMinY() );
    	    double maxHeight =Math.max( mapHeight, bounds.getHeight() );
    	    
    	    double topMargin =Math.abs( -getMaxY() -bounds.getMinY() );
    	    maxHeight+=topMargin;
    	    
    	    logger.debug( "topMargin=" + topMargin );
    	    
        	height =(int)( maxHeight * getMapToScreenFactor() );
        	
        } else {
    	    double mapWidth =Math.abs( getMaxX() - getMinX() );
    	    double maxWidth =Math.max( mapWidth, bounds.getWidth() );
    	    
    	    double leftMargin =Math.abs( bounds.getMinX() - getMinX() );
    	    maxWidth+=leftMargin;
    	    
    	    logger.debug( "leftMargin=" + leftMargin );    	    
    	    
        	width =(int)( maxWidth * getMapToScreenFactor() );
        }
        
        dim.setSize( width, height );
        
        logger.debug( "\nfixedWidth=" + width + ", fixedHeight=" + height + "\nbounds=" + bounds );
		
		return dim;
	}
	
	public Dimension getContainerDimension( Container container ) {
        Dimension dim =new Dimension();
        
	    if ( container == null ) {
	    	dim.setSize( 0d, 0d );
	    } else {
	    	Object obj =container.getParent().getParent();
	    	
	    	if ( obj instanceof JScrollPane ) {
	    		JScrollPane scrollPane =(JScrollPane)obj;
	    		JViewport viewPort =scrollPane.getViewport();
	        
	    		double w =(int)viewPort.getSize().getWidth() - (int)scrollPane.getVerticalScrollBar().getWidth();
	    		double h=(int)viewPort.getSize().getHeight() - (int)scrollPane.getHorizontalScrollBar().getHeight();    
	
	    		dim.setSize( w, h );
	    	
	    	// Multi layer window
	    	} else {
	    		dim.setSize( container.getWidth(), container.getHeight() );
	    	}
	    }
        
        return dim;
	}
	
	public double processMapFactor( Container container ) {
		Dimension dim =getContainerDimension( container );
		if ( dim.getWidth() == 0d && dim.getHeight() == 0d ) {
			return 0d;
		}
        
		return processMapFactor( dim.getWidth(), dim.getHeight() );
	}
	
    public void fitToWindow( Container container ) {
        JScrollPane scrollPane =(JScrollPane)container.getParent().getParent();
        JViewport viewPort =scrollPane.getViewport();
        
        Dimension dim =new Dimension(
                (int)viewPort.getSize().getWidth() - (int)scrollPane.getVerticalScrollBar().getWidth() ,
                (int)viewPort.getSize().getHeight() - (int)scrollPane.getHorizontalScrollBar().getHeight() );
        logger.debug( "view port size=" + dim );
        
	    // For scrolling purpose
	    ((JPanel)container).setSize( dim ); 	    
	            
	    ((JPanel)container).revalidate();
	    zoomFactor =DEFAULT_ZOOM_FACTOR;
	    
	    bounds =new Rectangle2D.Double( 0, 0, 0, 0 );
        
        processMapFactor( container );     
        
        container.repaint();
    }	
    
	/**
	 * Revalidate the scroll pane in case of bounds or zoom modification
	 * This is to manage scroll bars correctly
	 *
	 */
	public void revalidateScrollPane( Container container ) {    
		
		double maxWidth =Math.max( Math.abs( maxX - minX ), bounds.getWidth() );
		double maxHeight =Math.max( Math.abs( maxY - minY ), bounds.getHeight() );		
		
	    Dimension dim =new Dimension(
	    		 (int)( maxWidth * getMapToScreenFactor()), 
	    		 (int)( maxHeight * getMapToScreenFactor()) );		
	    
	    // For scrolling purpose
	    ((JPanel)container).setPreferredSize( dim ); 
	            
	    ((JPanel)container).revalidate();
	}    
	
	public Point2D convertScreenToMap( Point2D pt ) {
	    AffineTransform at =new AffineTransform();
	    
		at.scale( getScreenToMapFactor(), getScreenToMapFactor() );
		
		Point2D scaledPt2d =at.transform( pt, null );
		scaledPt2d.setLocation( scaledPt2d.getX() + minX, scaledPt2d.getY() - maxY );

		return scaledPt2d;
	}
	
	public Point2D convertMapToScreen( Point2D pt ) {
	    AffineTransform at =new AffineTransform();
	    
		at.scale( getMapToScreenFactor(), getMapToScreenFactor() );
		at.translate( -minX, maxY );
		
		Point2D scaledPt2d =at.transform( pt, null );

		return scaledPt2d;
	}
	
	public double getMapToScreenFactor() {
	    return mapFactor * (double)zoomFactor / 100.d;
	}
	
	public double getScreenToMapFactor() {
	    return 1.d / getMapToScreenFactor();
	}	
	
	public void updateGeographicBox( double xmin, double xmax, double ymin, double ymax ) {
	    setMinX( xmin );
	    setMaxX( xmax );
	    setMinY( ymin );
	    setMaxY( ymax );
	}	
	
	public void setDefaultGeographicBox() {
		setMinX( 0 );
		setMaxX( mtk.getContainer().getSize().getWidth() );
		setMinY( 0 );
		setMaxY( mtk.getContainer().getSize().getHeight() );
	}
	
	public double getMapFactor() {
	    return mapFactor;
	}

	public double getMaxX() {
		return maxX;
	}

	public void setMaxX(double maxX) {
		this.maxX = maxX;
	}

	public double getMaxY() {
		return maxY;
	}

	public void setMaxY(double maxY) {
		this.maxY = maxY;
	}

	public double getMinX() {
		return minX;
	}

	public void setMinX(double minX) {
		this.minX = minX;
	}

	public double getMinY() {
		return minY;
	}

	public void setMinY(double minY) {
		this.minY = minY;
	}

	public int getZoomFactor() {
		return zoomFactor;
	}

	public void setZoomFactor(int zoomFactor, Container container ) {
		this.zoomFactor = zoomFactor;
		
		revalidateScrollPane( container );
	}
	
	public void zoomOut( Point2D pt, Container container ) {
		if ( getZoomFactor() > MINIMUM_ZOOM_FACTOR ) {
			zoom( pt, container, 1d/(double)ZOOM_FACTOR_STEP);
		}
	}
	
	public void zoomIn( Point2D pt, Container container ) {
		zoom( pt, container, ZOOM_FACTOR_STEP );
	}
	
	public void zoom( Point2D pt, Container container, double zoomStepFactor ) {
		setZoomFactor( (int)((double)getZoomFactor() * zoomStepFactor), container );
		
        JScrollPane scrollPane =(JScrollPane)container.getParent().getParent();
        JViewport viewPort =scrollPane.getViewport();
		
		Dimension viewSize =viewPort.getPreferredSize();
		Dimension extentSize =viewPort.getExtentSize();	
			
		Point pt2 =new Point( (int)(pt.getX() * zoomStepFactor), (int)(pt.getY() * zoomStepFactor) );
		Point pt3 =viewPort.toViewCoordinates( pt2 );
		
		int x =(int)(pt3.getX() - (extentSize.getWidth()/2d) );
		int y =(int)(pt3.getY() - (extentSize.getHeight()/2d) );	
		
        if ( x < 0 ) {
        	x =0;
        }
        
        if ( y < 0 ) {
        	y =0;
        }
        
        if ( x + extentSize.width > viewSize.width ) {
        	x =Math.max( 0, viewSize.width - extentSize.width );
        	logger.debug("trucating view horizontally" );
        }
        
        if ( y + extentSize.height > viewSize.height ) {       	
        	y =Math.max( 0, viewSize.height - extentSize.height );
        	logger.debug("trucating view vertically" );        	
        }    			
		
        // This line is critical, don't remove it !
		viewPort.setViewPosition( new Point(0,0) );
		
        viewPort.scrollRectToVisible( new Rectangle( x, y
			     					  			   , extentSize.width, extentSize.height ) );			
		
		container.repaint();

	}

	public void setMapFactor(double mapFactor) {
		this.mapFactor = mapFactor;
	}

	public Rectangle2D getBounds() {
		return bounds;
	}

	public void setBounds(Rectangle2D bounds) {
		this.bounds = bounds;
	}
	
	private void enlargeGeographicBoxIfNecessary( Rectangle2D bounds ) {
		Rectangle2D geoBox =new Rectangle2D.Double();
		geoBox.setFrameFromDiagonal( minX, minY, maxX, maxY );
		
		// Enlarge the geographic box if user draws stuff outside of it
		if ( !geoBox.contains( bounds ) ) {
			geoBox.add( bounds );
			minX =geoBox.getMinX();
			minY =geoBox.getMinY();
			maxX =geoBox.getMaxX();
			maxY =geoBox.getMaxY();
		}		
	}

	public boolean isBoundsHaveChanged() {
		return boundsHaveChanged;
	}
	
	/**
	 * Have bounds of the map changed since the previous repaint ?
	 */
	public void manageBounds( Rectangle2D newBounds, Container container ) {
		boundsHaveChanged =( ( !newBounds.equals( getBounds() ) ) );
		if ( boundsHaveChanged ) {
			setBounds( newBounds );
			//enlargeGeographicBoxIfNecessary( newBounds );

		    if ( container != null ) {
		    	revalidateScrollPane( container );
		    }
		}
		
	}
	
    public void initializeGeoBoxUsingBackgroundImage( Container container, BackgroundImageShape bis ) {
    	
    	if ( getMinX() == 0 && getMaxX() == 0 && getMinY() == 0 && getMaxY() == 0 && container != null ) {

            JScrollPane scrollPane =(JScrollPane)container.getParent().getParent();
            JViewport viewPort =scrollPane.getViewport();
            
            double w =(int)viewPort.getSize().getWidth() - (int)scrollPane.getVerticalScrollBar().getWidth();
            double h=(int)viewPort.getSize().getHeight() - (int)scrollPane.getHorizontalScrollBar().getHeight(); 
    		
    		int width =(int)(w * getMapToScreenFactor() );//getMapFactor() * ( (double)getZoomFactor() / 100d ));
    		double ratio =bis.getImageHeight() != 0 ? (double)bis.getImageWidth() / (double)bis.getImageHeight()
    										 		: 1.d;	
    		int height=(int)((double)width / ratio);
    		
    		updateGeographicBox(0, (double)width, 0, (double)height );
    	}
    }
    
    /**
     * Compute factor to convert from digitizer coordinates to map coordinates
     * @param xMinD
     * @param yMinD
     * @param xMaxD
     * @param yMaxD
     * @see processMapFactor
     */
    public void computeDigitizerFactors( double xMinD, double yMinD, double xMaxD, double yMaxD ) {    	
	    double mapWidth =Math.abs( getMaxX() - getMinX() );
	    double width =Math.abs( xMaxD - xMinD );
        
        if ( mapWidth != 0 ) {
        	digitFactorX = width / mapWidth;
        } else {
        	digitFactorX = width;
        }
        
        if ( digitFactorX == 0 ) {
        	digitFactorX =1;
        }
        
	    double mapHeight =Math.abs( getMaxY() - getMinY() );
	    double height =Math.abs( yMaxD - yMinD );
        
        if ( mapHeight != 0 ) {
        	digitFactorY = height / mapHeight;
        } else {
        	digitFactorY = height;
        }
        
        if ( digitFactorY == 0 ) {
        	digitFactorY =1;
        }
        
        logger.debug( "digitFactorX=[" + digitFactorX + "], digitFactorY=[" + digitFactorY + "]" );        
    }
    
	public Point2D convertDigitizerToMap( Point2D pt ) {
		return new Point2D.Double( ( pt.getX() / digitFactorX ) + minX, ( pt.getY() / digitFactorY ) - maxY );
	}
	
}
